# -*- mode: python; coding: utf-8; -*-
##---------------------------------------------------------------------------##
##
## Copyright (C) 1998-2003 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##---------------------------------------------------------------------------##
"""
Wdigets for tile/tk.

Config option dialogs for tile/tk.
"""
__docformat__ = "restructuredtext en"
_ = lambda x: x
n_ = lambda x: x

import functools
from tkinter import Toplevel, Menu as _tkMenu, Text as tkText
import tkinter.ttk
import time

from . import constants as vcnst


def after_timeout(func):
    """
    after_timout(func:func) -> func

    Decorator to cancel timers before a class method.
    """

    @functools.wraps(func)
    def _after_timeout(cls, *args, **kwargs):
        """
        _after_timeout(cls:Object, *args, **kwargs) - func

        Call after_cancel on class timers for enter and cancel events.
        """
        if cls.timer['enter']:
            cls._widget.after_cancel(cls.timer['enter'])
        if cls.timer['cancel']:
            cls._widget.after_cancel(cls.timer['cancel'])
        return func(cls, *args, **kwargs)

    return _after_timeout



_MENUPATH = {}


class Menubar(_tkMenu, object):
    """
    Menubar that sets a gtk path like value to access menu items later.
    """

    def __init__(self, master, **kwargs):
        """
        __init__(master:Widget, name:str[, tearoff:bool=0, **Menu keywords**])
          -> None

        Initailize menu setting name and index, then call parent method in Menu.
        """
        self.__name = kwargs['name']
        self.__index = kwargs['tearoff'] = int(kwargs.get('tearoff', 0))
        super(Menubar, self).__init__(master, **kwargs)

    def _label_to_name(self, label):
        """
        _label_to_name(label:str) -> seq(str, str, int)

        Convert label to gtk path like value. Returns path, label, label index
        to underline accel char.
        """
        ##print label, type(label)
        import re
        name = re.sub(r'[^0-9a-zA-Z]', '', label).lower()
        label = _(label)
        underline = label.find('&')
        if underline >= 0:
            label = label.replace('&', '')
        return name, label, underline

    def add(self, item_type, cnf=None):
        """
        add(item_type:str, cnf={}) -> None

        Augment parent Menu add item to process label for gtk like path and the
        underline char index for accel. Then call parent method in Menu.
        """
        cnf = cnf or dict()
        label = cnf.get('label')
        if label:
            name = cnf.get('name')
            if not name:
                name, label, underline = self._label_to_name(label)
                cnf['underline'] = cnf.get('underline', underline)
            cnf['label'] = label
            if name:
                self._add_path(path='{0._w}.{1}'.format(self, name),
                              menu=self, index=self.__index,
                              submenu=cnf.get('menu'))
        if 'name' in cnf:
            del cnf['name'] # TclError: unknown option '-name'
        super(Menubar, self).add(item_type, cnf)
        self.__index += 1

    def _add_path(self, path, menu, index, submenu):
        """
        _add_path(path:str, menu:MfxMenu, index:int, submenu:MfxMenu) -> None

        gtk like path setter applied to MfxMenubar during menu creation.
        """
        #print path
        if path not in _MENUPATH:
            ##print path, menu, index, submenu
            _MENUPATH[path] = (menu, index, submenu)


class Menu(Menubar):
    """
    Menu that sets a gtk path like value to access menu items later.
    """

    def __init__(self, master, label, **kwargs):
        """
        __init__(master:Widget, name:str[, underline:int=0, **Menu keywords**])
          -> None

        Initailize menu setting name and underline, then call parent method in
        MfxMenuBar.
        """
        if not 'name' in kwargs:
            name, label, underline = self._label_to_name(label)
            kwargs['name'] = name
        else:
            underline = -1
        super(Menu, self).__init__(master, **kwargs)
        if not 'underline' in kwargs:
            kwargs['underline'] = underline
        if master:
            master.add_cascade(menu=self, label=label, **kwargs)


class SmartScrollbar(tkinter.ttk.Scrollbar, object):
    """
    Game intelligent scrollbars that hide when full canvas is viewable.
    """
    is_shown = False

    def set(self, first, last):
        """
        Set scrollbar slider to clients viewable portion.

        Override to check viewable portion. When portion covers entire client
        call grid remove method else call grid method. Final call next method.

        .. set(first, last) -> None
        """
        if float(first) <= 0 and float(last) >= 1:
            self.grid_remove()
        else:
            self.grid()
        super(SmartScrollbar, self).set(first, last)

    def grid(self, *args, **kwargs):
        """
        Set scrollbar grid.

        Override to set internal is shown value True. Final call next method.

        .. grid(*grid options*) -> None
        """
        self.is_shown = True
        super(SmartScrollbar, self).grid(*args, **kwargs)

    def grid_remove(self):
        """
        Unset scrollbar grid.

        Override to set internal is shown value False. Final call next method.

        .. grid_remove(*grid options*) -> None
        """
        self.is_shown = False
        super(SmartScrollbar, self).grid_remove()


class Tooltip(object):
    """
    Monitor mouse events for widget, creating tooltip with additional info.
    """
    __last_left = 0
    __text = None
    timer = {'enter': None, 'cancel': None}
    _tooltip = None
    __bind = {}
    _timeout = {'enter': 800, 'cancel': 5000, 'leave': 400}  # milliseconds

    class Widget(Toplevel, object):
        """
        Tooltip widget to give info for a widget.
        """
        def __init__(self, coord, text):
            """
            Init a tooltip widget.

            .. \__init__(self, coord, text) -> None
            """
            super(Tooltip.Widget, self).__init__()
            self.wm_iconify()
            self.wm_overrideredirect(1)
            self.wm_protocol('WM_DELETE_WINDOW', self.destroy)
            label = tkinter.ttk.Label(self, text=text, takefocus=False,
                              style='npsTip.TLabel')
            label.pack(ipadx=1, ipady=1)
            self.wm_geometry('%+d%+d' % (coord))
            self.wm_deiconify()

    def __init__(self, widget, text):
        """
        Init a tooltip for given widget binding mouse events for show / hide.

        .. \__init__(self, widget) -> None
        """
        self._widget = widget
        self.__text = text
        self.__bind['enter'] = widget.bind('<Enter>',
                                            lambda e: self._enter())
        self.__bind['leave'] = widget.bind('<Leave>',
                                            lambda e: self._leave())
        self.__bind['press'] = widget.bind('<ButtonPress>',
                                            lambda e: self._leave())

    def destroy(self):
        """
        Destory widget binding with tool tip.

        .. destroy() -> None
        """
        self._leave()
        if self.__bind and self._widget:
            for name, binding in list(self.__bind.items()):
                self._widget.unbind('<{}>'.format(name), binding)
            self.__bind = {}

    @after_timeout
    def _enter(self):
        """
        Handle mouse enter widget.

        Init internal tool tip widget. Set cancel timeout to destroy.
        Wait for timeout map leave value between widget enter and leave events
        before next init.

        .. \_enter() -> None
        """

        def _show_tip():
            """
            Create a widget tooltip.
            """
            self.timer['timer'] = None
            if not self.__text:
                return
            widget = self._widget
            xcoord = widget.winfo_pointerx()
            ycoord = widget.winfo_rooty() + widget.winfo_height() + 4
            self._tooltip = self.Widget((xcoord, ycoord), text=self.__text)
            self.timer['cancel'] = self._widget.after(self._timeout['cancel'],
                                                      self._leave)

        self.timer['cancel'] = None
        if time.time() - self.__last_left < self._timeout['leave'] / 1000.:
            _show_tip()
        else:
            self.timer['enter'] = self._widget.after(self._timeout['enter'],
                                                     _show_tip)

    @after_timeout
    def _leave(self):
        """
        Handle mouse leave widget.

        Set last left value to current time. Destroy tooltip. Reset timer map
        and tooltip.

        .. \_leave(event:Event=None) -> None
        """
        self.__last_left = time.time()
        if not self._tooltip is None:
            self._tooltip.destroy()
        self.timer['enter'] = self.timer['cancel'] = self._tooltip = None


class Toolbar(tkinter.ttk.Frame, object):
    """
    Applications should call show/hide after constructor.
    """
    _regions = {'buttons': {}, 'labels': {}}
    _region_seq = []
    __widgets = {'buttons': {}, 'labels': {}, 'tooltips': {}}
    __widget_seq = []
    __top_geometry = _toolbar_var = None
    __cache = []

    def connect_top(self, top, menubar=None):
        """
        Connect toolbar with TopLevel.

        Place toolbar widget. Populate toolbar with widgets from regions map
        data. Add a right click popup menu for the toolbar to configure. Bind
        players label to call the player options conf dialog on left click.

        .. connect_app(top:tkTopLevel) -> None
        """
        self.__top_geometry = top.wm_geometry
        super(Toolbar, self).__init__(top, class_='Toolbar', relief=vcnst.FLAT,
                                      borderwidth=0)

        # popup toolbar config menu
        if menubar is not None:
            popup = Menu(master=None, label=n_('Toolbar'), tearoff=0)
            menubar._add_toolbar_menu(popup)
            tool_menu = lambda e: popup.tk_popup(e.x_root, e.y_root)
            conf_menu = lambda e: menubar._config_player_options()
        else:
            tool_menu = conf_menu = lambda e: None

        # create toolbar widgets
        for name in self._region_seq:
            if name is None:
                widget = tkinter.ttk.Separator(self, takefocus=False,
                                       orient=vcnst.VERTICAL)
                self.__widget_seq.append(widget)
            else:
                self._add_region(name)

        # weight players label to consume left over space.
        position = self._region_seq.index('player')
        self.rowconfigure(position, weight=1)
        self.columnconfigure(position, weight=1)

        self.__widgets['labels']['player'].bind('<1>', conf_menu)

        self.bind('<3>', tool_menu)

    def __find_widget(self, name):
        """
        Returns toolbar widget either Button/CheckButton or Label.

        .. \__find_widget(name:str) -> Widget
        """
        if name in self._regions['buttons']:
            return self.__widgets['buttons'][name]

        elif name in self._regions['labels']:
            return self.__widgets['labels'][name]

    def _update_image(self, name, compound=None, style='default', size='small'):
        """
        Update region image.

        When given compound not text, call image cache load toolbar method given
        name, style and size to update widget and append a disabled image, too.

        .. \_update_image(name:str[, compound:str=None, style:str='default',
             size:str='small']) -> None
        """
        if compound == 'text':
            self.config_region(name, image=None)
            return

        from ..utillib.cache import IMAGE_CACHE
        image = IMAGE_CACHE.load_toolbar_image(name, style, size)
        self.__cache.append(image.unavailable)  # Prevent GC of image
        self.config_region(name, image=(image, 'disabled', self.__cache[-1]),
                           compound=compound)

    def _add_region(self, name):
        """
        Create toolbar widget region.

        Set either a Button/CheckButton or Label region. Uses internal maps
        for buttons and labels to apply widget details. Optionally adds toolip
        based on given details as well when included.

        .. \_add_region(name:str) -> None
        """
        if name in self._regions['buttons']:
            kwargs, tooltip = self._regions['buttons'][name]
            widget_ = tkinter.ttk.Checkbutton if 'variable' in kwargs else tkinter.ttk.Button
            widget = widget_(self, takefocus=False, style='TLabel', **kwargs)
            self.__widgets['buttons'][name] = widget

            self._update_image(name)

        elif name in self._regions['labels']:
            kwargs, tooltip = self._regions['labels'][name]
            widget = tkinter.ttk.Label(self, relief='ridge', justify='center',
                               wraplength=100, **kwargs)
            self.__widgets['labels'][name] = widget

        else:
            return

        self.__widget_seq.append(widget)
        self.show_region(name)

        if tooltip:
            self.__widgets['tooltips'][name] = Tooltip(widget, tooltip)

    def _show_widget(self, widget, side=1, **kwargs):
        """
        Show region widget on toolbar.

        Set widget packing based on toolbar side.

        .. \_show_widget(widget, side:int=1[, **grid keywords**]) -> None
        """
        # Visible dock state ?
        if side not in (1, 2, 3, 4,):
            return False

        position = self.__widget_seq.index(widget)

        # Horz visible dock state ?
        if side in (1, 2,):
            kwargs.update({'row': 0, 'column': position})

        # Vert visible dock state ?
        else:
            kwargs.update({'row': position, 'column': 0})

        widget.grid(sticky='nsew', **kwargs)
        return True

    def show_region(self, name, show=None):
        """
        Show/Hide region button or label.

        Call show widget method to show. Otherwise call widget grid forget
        method to hide. Update region map state. Final call update seperator
        method to adjust seperators visible.

        .. show_region(name:string, show:bool=*option*) -> None
        """
        widget = self.__find_widget(name)

        if (show if not show is None else self._toolbar_var[name]):
            if not self._show_widget(widget, padx=0, pady=1):
                widget.grid_forget()
        else:
            widget.grid_forget()

        self._toolbar_var[name] = show
        self.__update_seperator()


    def config_region(self, name, **kwargs):
        """
        Config region options.

        Call region of given name config method with given keyword options.

        .. config_region(name:str[, **Label keywords**]) -> None
        """
        widget = self.__find_widget(name)
        if widget is not None:
            widget.config(**kwargs)

    def show(self, side=1, resize=True):
        """
        Show toolbar on given side.

        Sets toolbar grid config method for given side. Update region widgets.

        .. show(side:int=1, resize:bool=True) -> None
        """
        if resize:
            self.__top_geometry('')    # cancel user-specified geometry

        kwargs = {# place top
                  1: {'row': 0, 'column': 1, 'sticky': vcnst.STICKY_EW,
                        'padx': 0, 'pady': 0},
                  # place bottom
                  2: {'row': 2, 'column': 1, 'sticky': vcnst.STICKY_EW,
                        'padx': 0, 'pady': 0},
                  # place left
                  3: {'row': 1, 'column': 0, 'sticky': vcnst.STICKY_NS,
                        'padx': 0, 'pady': 1},
                  # place right
                  4: {'row': 1, 'column': 2, 'sticky': vcnst.STICKY_NS,
                        'padx': 0, 'pady': 1}}.get(side)

        if kwargs is not None:
            self.grid_configure(**kwargs)
        else:
            self.grid_forget()

        if side > 0:
            for name in self._region_seq:
                if name and self._toolbar_var[name]:
                    widget = self.__find_widget(name)
                    if not self._show_widget(widget, padx=0, pady=1):
                        widget.grid_forget()

    def __update_seperator(self):
        """
        Update seperators between regions.

        Hide any when doubled or on an edge. Call show widget method to show.
        Otherwise call widget grid forget method to hide.

        .. \__update_seperator() -> None
        """
        for index, widget in enumerate(self.__widget_seq):
            if isinstance(widget, tkinter.ttk.Separator):
                prior_ = self._region_seq[index - 1]
                if prior_ is not None or self._toolbar_var[prior_]:
                    if not self._show_widget(widget, padx=4, pady=6):
                        widget.grid_forget()
                else:
                    widget.grid_forget()

    def update_region_state(self, **kwargs):
        """
        Update button region state.

        .. update_region_state([**button region names and values**]) -> None
        """
        widgets = self.__widgets['buttons']
        for key, value in ([k, v] for k, v in list(kwargs.items()) if k in widgets):
            self.config_region(key, state=value)

    _format_text = lambda s, k, v: v or ''

    def update_region_text(self, **kwargs):
        """
        Update label region texts.

        .. update_region_text([**label region names and values**]) -> None
        """
        widgets = self.__widgets['labels']
        for name, value in ([k, v] for k, v in list(kwargs.items()) if k in widgets):
            self.config_region(name, text=self._format_text(name, value))

    def update_region_image(self, **kwargs):
        """
        Update button region images.

        .. update_region_image([**button image values**]) -> None
        """
        for name in self.__widgets['buttons']:
            self._update_image(name, **kwargs)


class Statusbar(tkinter.ttk.Frame, object):
    """
    Create a basic app status bar with tile/Tk.
    """
    _regions = []
    __show = True
    __widgets = {'labels': {}, 'tooltips': {}}
    __column = 0
    __top_geometry = None
    _label_kwargs = {'anchor': 'center', 'borderwidth': 1, 'relief': 'sunken'}
    _frame_kwargs = {'row': 0, 'column': 0, 'columnspan': 1}
    top = None

    def __init__(self, row, column, columnspan):
        """
        __init__(self, row, column, columnspan) -> None

        Set the frame gridding for the status bar frame. The parent method will
        be called in connect app.
        """
        self._frame_kwargs = {'row': row, 'column': column,
                              'columnspan':  columnspan}
        super(Statusbar, self).__init__()

    def connect_top(self, top, grip=False):
        """
        connect_app(top:TopLevel, grip:bool=False) -> None

        Populate frame with an inner frame that contains label regions based on
        object's regions attr. The objects's frame kwargs contains it grid info.
        For each regions a call to add region is made to create the region.
        """
        self.__top_geometry = top.wm_geometry

        super(Statusbar, self).__init__(top)
        self.grid(sticky='ew', **self._frame_kwargs)

        frame = tkinter.ttk.Frame(self)
        frame.pack(side='left', expand=True, fill='both', padx=0, pady=1)

        for name, tip, width in self._regions:
            self._add_region(frame, name, tooltip=tip, width=width)
        self._add_region(frame, 'info', expand=True)
        self.config_region('info', padding=(8, 0))

        if grip:
            grip_ = tkinter.ttk.Sizegrip(self)
            grip_.pack(side='right', anchor='se')

    def _add_region(self, parent, name, expand=False, width=0, tooltip=None):
        """
        _add_region(parent:Widget, name:str, expand:bool=False, width:int=0,
          tooltip:Tooltip=None) -> None

        Create a label region to hold text with a tooltip that optional expands.
        """
        label = tkinter.ttk.Label(parent, width=width, **self._label_kwargs)
        label.grid(row=0, column=self.__column, sticky='nsew', padx=1)
        self.__widgets['labels'][name] = label

        if expand:
            parent.grid_columnconfigure(self.__column, weight=1)

        self.__column += 1

        if tooltip:
            self.__widgets['tooltips'][name] = Tooltip(label, tooltip)

    def show_region(self, name, show=None):
        """
        show_region(name:str, show:bool) -> None

        Show or hide a label region by name given.
        """
        widget = self.__widgets['labels'][name]
        if show:
            widget.grid()
        else:
            widget.grid_remove()

    def config_region(self, name, **kwargs):
        """
        config_region(name:str[, **tkLabel keywords** ]) -> None

        Config a label region by name given passing tkLable keywords and values.
        """
        self.__widgets['labels'][name].config(**kwargs)

    def show(self, show=True, resize=False):
        """
        show(show:bool=True, resize:bool=False) -> bool

        Show or hide statusbar frame. Called by menuopts config statusbar place
        and during a sub classed app connect.
        """
        if self.__show == show:
            return False

        if resize:
            self.__top_geometry('')    # cancel user-specified geometry

        if not show:
            self.grid_forget()
        else:
            self.grid(sticky='ew', **self._frame_kwargs)
        self.__show = show

        return True

    _format_text = lambda s, k, v: v or ''

    def update_region_text(self, **kwargs):
        """
        update_region_text([**label region names and values**]) -> None

        Update label region text by key / value pairs. Each key must be a label
        region with its corresponding new text value to configure. The value is
        passed through format text before calling config region for key, value.
        """
        for key, value in list(kwargs.items()):
            if not key in self.__widgets['labels']:
                continue
            self.config_region(key, text=self._format_text(key, value))


class ScrolledTextFrame(tkText, object):

    kwargs = {'relief': vcnst.SUNKEN,
              'wrap': vcnst.WORD,
              'width': 64,
              'height': 16}

    def __init__(self, parent, **kwargs):
        frame = tkinter.ttk.Frame(parent)
        self._kwargs.update(kwargs)
        super(ScrolledTextFrame, self).__init__(frame, **kwargs)
        self.grid(row=0, column=0, sticky=vcnst.STICKY_NSEW)
        scrollbar = tkinter.ttk.Scrollbar(frame, orient=vcnst.VERTICAL,
                                  command=self.yview)
        scrollbar.grid(row=0, column=1, sticky=vcnst.STICKY.NS)
        self.configure(yscrollcommand=scrollbar.set)
        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(0, weight=1)
        self._frame = frame
        self._scrollbar = scrollbar